1 <?php
2 if (!defined('PASSWORD_DEFAULT')) {
3 define('PASSWORD_BCRYPT', 1);
4 define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
5 }
6
7 Class Password {
8
9 public function __construct() {}
10
11
12 /**
13 * Hash the password using the specified algorithm
14 *
15 * @param string $password The password to hash
16 * @param int $algo The algorithm to use (Defined by PASSWORD_* constants)
17 * @param array $options The options for the algorithm to use
18 *
19 * @return string|false The hashed password, or false on error.
20 */
21 function password_hash($password, $algo, array $options = array()) {
22 if (!function_exists('crypt')) {
23 trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
24 return null;
25 }
26 if (!is_string($password)) {
27 trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
28 return null;
29 }
30 if (!is_int($algo)) {
31 trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
32 return null;
33 }
34 switch ($algo) {
35 case PASSWORD_BCRYPT :
36 // Note that this is a C constant, but not exposed to PHP, so we don't define it here.
37 $cost = 10;
38 if (isset($options['cost'])) {
39 $cost = $options['cost'];
40 if ($cost < 4 || $cost > 31) {
41 trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
42 return null;
43 }
44 }
45 // The length of salt to generate
46 $raw_salt_len = 16;
47 // The length required in the final serialization
48 $required_salt_len = 22;
49 $hash_format = sprintf("$2y$%02d$", $cost);
50 break;
51 default :
52 trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
53 return null;
54 }
55 if (isset($options['salt'])) {
56 switch (gettype($options['salt'])) {
57 case 'NULL' :
58 case 'boolean' :
59 case 'integer' :
60 case 'double' :
61 case 'string' :
62 $salt = (string)$options['salt'];
63 break;
64 case 'object' :
65 if (method_exists($options['salt'], '__tostring')) {
66 $salt = (string)$options['salt'];
67 break;
68 }
69 case 'array' :
70 case 'resource' :
71 default :
72 trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
73 return null;
74 }
75 if (strlen($salt) < $required_salt_len) {
76 trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING);
77 return null;
78 } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
79 $salt = str_replace('+', '.', base64_encode($salt));
80 }
81 } else {
82 $buffer = '';
83 $buffer_valid = false;
84 if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
85 $buffer = mcrypt_create_iv($raw_salt_len, MCRYPT_DEV_URANDOM);
86 if ($buffer) {
87 $buffer_valid = true;
88 }
89 }
90 if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
91 $buffer = openssl_random_pseudo_bytes($raw_salt_len);
92 if ($buffer) {
93 $buffer_valid = true;
94 }
95 }
96 if (!$buffer_valid && is_readable('/dev/urandom')) {
97 $f = fopen('/dev/urandom', 'r');
98 $read = strlen($buffer);
99 while ($read < $raw_salt_len) {
100 $buffer .= fread($f, $raw_salt_len - $read);
101 $read = strlen($buffer);
102 }
103 fclose($f);
104 if ($read >= $raw_salt_len) {
105 $buffer_valid = true;
106 }
107 }
108 if (!$buffer_valid || strlen($buffer) < $raw_salt_len) {
109 $bl = strlen($buffer);
110 for ($i = 0; $i < $raw_salt_len; $i++) {
111 if ($i < $bl) {
112 $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
113 } else {
114 $buffer .= chr(mt_rand(0, 255));
115 }
116 }
117 }
118 $salt = str_replace('+', '.', base64_encode($buffer));
119 }
120 $salt = substr($salt, 0, $required_salt_len);
121
122 $hash = $hash_format . $salt;
123
124 $ret = crypt($password, $hash);
125
126 if (!is_string($ret) || strlen($ret) <= 13) {
127 return false;
128 }
129
130 return $ret;
131 }
132
133 /**
134 * Get information about the password hash. Returns an array of the information
135 * that was used to generate the password hash.
136 *
137 * array(
138 * 'algo' => 1,
139 * 'algoName' => 'bcrypt',
140 * 'options' => array(
141 * 'cost' => 10,
142 * ),
143 * )
144 *
145 * @param string $hash The password hash to extract info from
146 *
147 * @return array The array of information about the hash.
148 */
149 function password_get_info($hash) {
150 $return = array('algo' => 0, 'algoName' => 'unknown', 'options' => array(), );
151 if (substr($hash, 0, 4) == '$2y$' && strlen($hash) == 60) {
152 $return['algo'] = PASSWORD_BCRYPT;
153 $return['algoName'] = 'bcrypt';
154 list($cost) = sscanf($hash, "$2y$%d$");
155 $return['options']['cost'] = $cost;
156 }
157 return $return;
158 }
159
160 /**
161 * Determine if the password hash needs to be rehashed according to the options provided
162 *
163 * If the answer is true, after validating the password using password_verify, rehash it.
164 *
165 * @param string $hash The hash to test
166 * @param int $algo The algorithm used for new password hashes
167 * @param array $options The options array passed to password_hash
168 *
169 * @return boolean True if the password needs to be rehashed.
170 */
171 function password_needs_rehash($hash, $algo, array $options = array()) {
172 $info = password_get_info($hash);
173 if ($info['algo'] != $algo) {
174 return true;
175 }
176 switch ($algo) {
177 case PASSWORD_BCRYPT :
178 $cost = isset($options['cost']) ? $options['cost'] : 10;
179 if ($cost != $info['options']['cost']) {
180 return true;
181 }
182 break;
183 }
184 return false;
185 }
186
187 /**
188 * Verify a password against a hash using a timing attack resistant approach
189 *
190 * @param string $password The password to verify
191 * @param string $hash The hash to verify against
192 *
193 * @return boolean If the password matches the hash
194 */
195 public function password_verify($password, $hash) {
196 if (!function_exists('crypt')) {
197 trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
198 return false;
199 }
200 $ret = crypt($password, $hash);
201 if (!is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) {
202 return false;
203 }
204
205 $status = 0;
206 for ($i = 0; $i < strlen($ret); $i++) {
207 $status |= (ord($ret[$i]) ^ ord($hash[$i]));
208 }
209
210 return $status === 0;
211 }
212
213 }